什么是DAL,data access layer

 

DAL 的职责是:

封装所有数据访问和数据相关的业务规则,为上层提供统一的数据接口。

例如:

而不是:

什么是DAL?

假设你的项目是这样的:

真正的 DAL 通常在:

例如:

这一整个文件:

就是:

Users Data Access Layer

不是里面某一个函数。

其余的posts.ts,orders.ts同样是DAL。

定义

What is a Data Access Layer?

A Data Access Layer is the application's single entry point for accessing data. Instead of letting pages or Server Actions talk to Prisma directly, all data-related concerns—such as database queries, authentication, authorization, caching, error handling, and business rules—are encapsulated behind reusable data access functions like getUsers() or createOrder().

为什么需要 DAL?

1. 避免每个页面都直接操作数据库

例如在多个页面可能都会写这段代码,如果有修改呢?岂不是要改很多地方。

DAL:

以后:

统一维护。

2. 集中权限控制(最重要)

这是 Next.js 官方最推荐 DAL 的原因。

不推荐在页面里面写权限控制代码:

任何页面都可能忘记权限。

DAL:

以后所有地方:

权限永远一致。

 

3. Server Component 可以直接调用

例如:

Page 根本不知道:

它只知道:

符合:

4. 缓存统一管理(Next.js 16 推荐)

例如:

如果以后:

需要:

只改 DAL:

Page:

完全不用改。


数据更新:

整个缓存体系:

更新:

缓存策略集中管理,不会散落在每个页面里。

5. 更容易测试

Page:

测试:

Mock:

而不是 Mock:

为什么需要data access layer?

For medium to large Next.js projects, I usually introduce a Data Access Layer. Instead of letting Server Components or Server Actions access Prisma directly, I encapsulate database queries, authorization checks, and business rules into reusable functions. This improves code reuse, centralizes permission logic, makes caching easier to introduce, and keeps the presentation layer focused on rendering rather than data access.

中文:

对于中大型 Next.js 项目,我通常会引入 Data Access Layer,而不是让 Server Components 或 Server Actions 直接访问 Prisma。DAL 负责封装数据库查询、权限校验以及部分业务规则,这样可以提高代码复用率、统一权限控制、方便后续增加缓存,并让页面组件专注于 UI 渲染。

 

Kyle的DAL其实与基础版不同

它的两个文件helpers.ts和types.ts,其实不是完整的 DAL,更准确地说,它是 DAL 的基础设施(DAL Infrastructure) 或者 DAL Utilities。真正的DAL是我的data.ts这个文件,也可以根据featuer来区分多个文件,这多个文件就是DAL。


它们其实没有查询数据库。

例如:

它做的是:

不是:

再看:

它做的是:

它也没有:

所以它其实更像:

或者:

它真正的 DAL 长什么样?

如果按照他的架构。

真正查询用户可能是:

这里:

才是真正的:

里面用了:

这些 helper。

所以:

和:

不是一个东西。

 

为什么要这样拆?

因为如果:

都需要:

以及:

你难道复制:

二十遍?

当然不要。所以抽:

以后,所有 DAL共享。

该怎么设计呢?

我一般会分三层:

第一层

负责:

例如:

第二层

真正的数据访问:

里面调用:

第三层

Page:

根本不知道:

Kyle为什么叫它 DAL?

因为他的设计理念是:

DAL 不仅负责访问数据库,还负责数据访问过程中所有横切关注点(Cross-Cutting Concerns)。

这些包括:

所以:

这套设计实际上分成了两层:

第一层:DAL Infrastructure(公共能力)

负责所有横切关注点(Cross-Cutting Concerns):

这些函数本身不关心业务,可以被所有数据访问函数复用。

第二层:Business Data Access Layer(业务数据访问)

例如:

这些函数负责真正访问数据库,它们组合使用第一层提供的公共能力,最终形成整个应用唯一的数据访问入口。

这也是 Web Dev Simplified 这套 DAL 架构的核心思想:业务代码只负责业务,认证、错误处理、返回格式等公共逻辑全部下沉到 DAL Infrastructure 中统一管理。

 

 

Kyle的代码最难理解的地方

其实DAL最难理解的就是,函数里面嵌套函数,将函数抽象为参数传递到函数中,并且函数参数里面还有参数,这是我最陌生的地方。只要多接触,自然就熟悉了。

如果只是单纯的函数作为参数传递进来,我还是可以理解的:

但如果函数作为参数,并且函数参数里面定义有参数和返回值类型,我看上去就很难理解了:

其实仔细拆解一下,就会发现,operation就是一个函数,并且在调用operation函数的时候,可以接收到传递过来的session参数,就是这里难以理解。这就是闭包的概念。但是这里非常抽象,就是operation其实我不知道是什么样的,

data access layer(DAL) 代码拆解

参考webdevsimplied这个老师的做法。这个DAL是每个项目都必须的,必须把这个功能加上。

老师的项目使用了sqlite3,所以不需要专门的数据库,使用npm install,然后运行drizzle相关的命令即可。重点关注两个页面:/todos和/admin。

_naive_dal-basic_dal-advanced三种类型的文件,分别对应的是不同的data access layer写法,可以将其替换到page.tsx里面看效果。

image-20260121214809952

action函数也有data access layer,也是分为三种类型,可以对比看。

image-20260121215042463

初步分析

  1. naive写法

naive里面的代码就不要参考了,这是不推荐的写法,但是dal-advanced里面的封装太复杂了,这个暂时还看不懂,所以直接参考dal-basic的写法,能够写成这样就已经很不错了。dal-advanced里面的代码要慢慢学会写。

naive的写法就是在页面中直接进行用户权限检查和数据库查询,这样会导致大量的样板代码,如果直接在页面查询,很容易忘记添加权限检查,从而导致安全漏洞(例如普通用户访问了管理员数据)。反之将数据获取逻辑封装在函数中,可以更方便地统一应用 Next.js 的缓存机制(如 cacherevalidateTag)。

下面这样的代码就不要写了:

image-20260122143447290

  1. dal-basic写法

最起码要写成这样的,要先封装一个函数,然后在页面中调用(也就相当于把检查用户是否登录的语句放到封装函数里面了):

image-20260428142334417

image-20260122143644575

 

仔细了解一下之后,好像dal-basic也太简单了一点,dal-basic的mutations和queries里面对user的校验代码也很重复,而且还是手动抛出错误,但就是简单,别人一看代码就明白了。

  1. dal-advanced写法

dal-advanced的代码封装,太抽象了一点,我看了很久,很难总结出它到底做了什么,我问一下AI:

在视频中,Kyle 介绍的 dal-advanced(高级数据访问层)模式,核心思想是将数据获取逻辑从“直接抛出错误/重定向”转变为“返回结构化的结果对象”

这种模式模仿了类似 Rust 的 Result 类型或验证库(如 Zod)的返回结构。以下是该模式代码设计的详细拆解:

  1. 核心数据结构:Result 对象

dal-advanced 中,DAL 函数不再直接返回数据,而是返回一个包含状态的对象。其 TypeScript 定义大致如下:

为什么要这么做?

  1. 标准化的 DAL 函数写法

一个高级 DAL 函数(如获取用户待办事项)的代码结构通常如下:

  1. 调用层的灵活处理(分层消费)

这是 dal-advanced 最强大的地方,不同的调用方可以根据返回的 error 做出不同反应:

场景 A:在 React Server Component (页面) 中

如果你希望用户没登录就直接跳到登录页,可以使用一个辅助函数 verifyData

场景 B:在 API Route 或 Server Action 中

如果你正在处理一个异步请求,不希望页面跳转,而是想给前端一个 JSON 错误:

  1. 进阶:使用 Wrapper 减少冗余

视频中还提到了创建一个通用的包装器(如 dbWrapper),自动处理数据库错误捕获:

总结 dal-advanced 的代码优势:

  1. 强类型支持:通过 TypeScript,你在处理 result.data 之前必须先判断 result.success,减少了空指针崩溃。
  2. 错误分类:将“未登录”、“无权限”和“数据库崩溃”明确区分开。
  3. 多环境复用:同一套 DAL 代码可以无缝用于页面渲染、后台接口和自动化脚本,因为它的逻辑是“纯粹”的返回,不依赖于 Next.js 的路由环境。

这种模式虽然增加了一些样板代码(每个函数都要返回对象),但在中大型项目中,它带来的可测试性安全性远超基础模式。

 

这不就是我一直寻找的问题答案吗?我之前一直担心nextjs里面的数据操作,怎么处理错误?要不要写try...catch?写了try...catch之后,boilerplate代码太多了,显示不出我的能力啊。这个dal-advanced不就解决了问题嘛。

多看几遍,一遍肯定是看不懂的。看懂了之后,自己写几遍。

 

dal-advanced 的 helper里面的函数分析

这个文件里面是一套封装函数,这段代码本质上是一套工具集,目的是让你的 Next.js 应用在处理数据库请求时,既安全又灵活。

核心原理是:将数据操作变成一个可以被层层拦截、层层处理的“管道”

我们可以把这些函数拆分为 执行者 - 处理数据库(dalDbOperation)、动作适配器 - 处理结果(Redirect/Throw/VerifySuccess)、守门员/保镖(RequireAuth)和辅助工具(Format)。

第一类:执行者(处理原始数据库操作)

  1. dalDbOperation<T>(operation)

这是所有数据库操作的“防护壳”。

第二类:保镖(权限拦截)

  1. dalRequireAuth<T>(operation, options)

这是最核心的函数,用于定义一个受保护的数据操作。

第三类:动作适配器(在页面/Action 中消费结果)

这组函数决定了当 DAL 返回结果后,程序该表现出什么行为

  1. dalLoginRedirect(dalReturn)

  1. dalUnauthorizedRedirect(dalReturn, path)

  1. dalThrowError(dalReturn)

  1. dalVerifySuccess(dalReturn, options) —— 终极聚合函数

这是你在 Server Component(页面文件)里最常用的函数。

这是一个链式组合。它依次检查:是否没登录?(是就跳登录页) -> 是否没权限?(是就跳首页或其它页) -> 是否有其他错误?(是就直接抛异常)。

 

第四类:辅助工具

  1. dalFormatErrorMessage(error)

总结:这些函数是如何串联工作的?

为了帮你理清思路,请看下面这个逻辑流向图:

完整的使用流程如下:

  1. 定义层 (Data Layer)

    使用 dalRequireAuth 包装你的 SQL 查询,确保用户登录后才能操作SQL。使用dalDbOperation包裹数据库操作,使用try...catch捕获错误,将返回结果或者错误包装成对象返回。

  2. 处理层 (Action/Page)

    • 如果你在 Page 里,用 dalVerifySuccess:遇到错误自动重定向,成功直接拿数据。
    • 如果你在 Server Action 里,手动判断 result.success:如果失败,用 dalFormatErrorMessage 拿到文字发回给前端显示。

这样写的最大好处: 你的业务逻辑(SQL)只需要写一次,但在“直接跳页面”和“只显示报错文字”两个场景下都能复用。

  1. 逻辑流示意图

为了让你理解这些函数是怎么串联的,看这个流程:

UI 发起请求 调用一个 DAL 函数。

DAL 函数内部 调用 dalRequireAuth

dalRequireAuth 检查权限 失败则返回错误对象 成功则调用 dalDbOperation

dalDbOperation 执行 SQL 成功则返回 { success: true, data }

UI 拿到结果 调用 dalVerifySuccess

 

  1. 为什么这样写是“资深”?

这段代码解决了一个 Next.js 开发中的巨大难题:灵活性

总结

 

dal-advanced 怎么显示错误?

这是一个非常棒的问题,它触及了 dal-advanced 模式的核心价值:灵活性

dal-advanced 并不强制你报错的方式,它只是给了你一份“标准化的诊断报告”,由你决定是在大屏幕上显示(Error 页面),还是弹一个小窗口(Toast)。


  1. 触发 error.tsx (全屏报错)

如果你希望 Next.js 捕捉到错误并跳转到错误页面,你会使用 dalThrowErrordalVerifySuccess

只要一 throw,Next.js 就会自动激活最近的 error.tsx。

  1. 触发 Toast (轻量提醒)

如果你在 Server Action(比如用户点击“收藏”按钮)中使用,你通常不希望整个页面刷新报错,而是想在角落弹出一个 Toast。


  1. 触发 redirect (自动跳转)

这是 dal-advanced 提供的第三种路径。


  1. 总结对比

我们可以通过下面这张图来看清错误流转的过程:

处理方式使用的函数最终 UI 表现适用场景
硬拦截dalVerifySuccesserror.tsx 页面必须有数据才能渲染的页面组件。
软处理dalFormatErrorMessageToast 或 表单文字按钮点击、表单提交等异步交互。
重定向dalLoginRedirect浏览器 URL 变化,跳转页面鉴权失败、权限不足。
  1. 为什么 Kyle 觉得这很“高级”?

因为在没有这个系统之前,如果你在数据库函数里写死了 throw new Error(),你就只能用 error.tsx 报错,没法轻松地改成用 Toast 提示。

有了 dal-advanced,数据库函数只管吐出结果,显示方式的选择权交给了调用它的那行代码。

 

 

一些动作适配器在定义请求方法的时候已经使用了(我的感觉是操作已经被保护好了),为什么页面中真正请求数据的时候,还要加上dalVerifySuccess呢?不重复吗

比如说这段代码:

已经使用dalDbOperationdalRequireAuth保护好了操作,如果有报错,那么就会返回error数据,为什么还需要使用dalVerifySuccess来检查一遍呢?我的感觉就是代码重复了。

image-20260428150253609

 

 

简单回答:因为如果报错了,dalDbOperationdalRequireAuth只能返回错误对象,没有办法对错误进行UI方面的处理。而dalVerifySuccess可以依据相应的错误类型,对错误进行UI上的处理,比如说返回login页面、或者给出toast提示并返回上一页等等。

以下是Gemini给出的回答:

这是一个非常敏锐的观察!初看之下,确实感觉“保镖”和“医生”做了两遍检查。

但实际上,这正是 dal-advanced 模式最精妙的地方:一个是逻辑拦截,一个是UI决策。 它们并不重复,而是互补的。

以下是深度拆解,告诉你为什么这两行代码缺一不可:


  1. dalRequireAuth(在 getAllTodos 内部)

角色:底线保镖

  1. 组件中的 if (res.error.type === "no-access")

角色:定制化 UI 策略

  1. dalVerifySuccess

角色:兜底清理员

  1. 为什么这不叫“重复”?

请看这个逻辑分流过程

如果你删掉其中一个会发生什么?


  1. 总结:资深开发者的思维链条

  2. 查询层(DAL):总是用 dalRequireAuth 封装,保证安全性

  3. 页面层(Page)

    • 我有特殊的 UI 需求吗? 有(比如想显示特定文字),那就手动 if 拦截掉。
    • 剩下的按默认规矩办:直接丢给 dalVerifySuccess。它会帮我把“没登录”的用户送走,把“报错”的用户交给错误页,最后顺手把干净的数据解构出来给我用。

作者在这里展示的是:如何利用 dal-advanced 模式,既保留了默认的重定向/报错功能,又能灵活地为特定错误提供自定义的 UI 渲染。

 

How to handle Errors Like a Senior 视频解析

https://www.youtube.com/watch?v=ovnyeq-Xxrc

视频中 Kyle (Web Dev Simplified) 按顺序演示了 5 个递进的错误处理版本,核心场景是 Next.js 项目(server actions / API routes)中处理登录、权限等错误(如 Unauthenticated、Unauthorized、ValidationError、Unexpected 等)。他使用了自定义错误类、权限检查、redirect vs JSON 响应等典型示例。

  1. Starting Code(00:54 - 02:21):初级/散乱方式(直接在 Action/Route 中处理)

直接在 server action 或 API route 里写业务逻辑 + try-catch,处理不同错误类型(redirect 或 return error message)。

典型代码(重建自演示风格)

哪里好

哪里不好

 

  1. Service Layer(04:37 - 13:49):引入服务层,重构业务逻辑

把核心业务 + 权限检查抽到独立 service 文件,service 只负责返回数据或 throw custom errors,不处理 redirect/JSON。

典型代码

哪里好

哪里不好

 

  1. More Problems(13:49 - 15:44):服务层后的剩余问题

Kyle 特别强调即使用了 service layer,问题依然存在:

好/不好:基本同上,只是更清晰地展示了为什么需要下一步。

 

  1. Simple Solution(15:44 - 26:02):自定义 Result 类型(discriminated union)

不再 throw,而是返回 Result 类型(success 或 failure),用 discriminated union 让 TS 自动 narrowing。

典型代码(视频中使用 tuple 风格,简化版)

哪里好

哪里不好

 

  1. Library Solution(26:02 - end):Neverthrow 库

引入 neverthrow(轻量,无依赖),提供 Ok/Err、match、andThen、map 等方法,支持 AsyncResult。

典型代码

还提到 ESLint 插件强制使用 Result(禁止直接 throw 或忽略错误)。

哪里好

哪里不好

总体:Kyle 的讲解非常清晰,从问题到方案递进强烈,推荐中高级 TS/Next.js 开发者观看。核心理念是“错误作为显式值 + 类型安全 + 架构分离”。没有专用 repo,代码是演示性质的。

 

 

服务端请求怎么调试

在 Next.js 的 App Router(Server Components) 中,服务端发出的 fetch 请求不会出现在浏览器的 Network 面板里,这是目前(2025年底)框架的设计使然。但还是可以手动输出一些日志来调试。

1、在 next.config.js 里打开 fetch 日志(Next.js 内置,最方便)

2、加上基本的错误捕获 + 丰富日志

你会看到类似这样清晰的输出: